package com.mybpm.core

import com.mybpm.core.instance.ProcessInstance
import com.mybpm.core.instance.ToDoTask
import grails.core.GrailsApplication
import grails.util.Holders
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service

import java.util.regex.Matcher
import java.util.regex.Pattern

/**
 * 流程脚本相关服务
 */
@Service
public class ProcessScriptService {
    private static final Logger log = LoggerFactory.getLogger(ProcessScriptService.class)

    @Autowired
    private BpmUserService userService
    @Autowired
    GrailsApplication grailsApplication = Holders.grailsApplication

    // 校验公式的正则表达式：以$开头，后面都是大写字母和_，以 空格()]}<>=&|?*,;$'"+- 等字符结尾
    static Pattern p = ~/(\$[A-Z][0-9A-Z_]+)([^0-9A-Z_a-z]|$)/

    /**
     * 执行脚本，并返回结果<p>
     *
     * @param script 脚本
     * @param bean 实体Bean
     * @param processInst 流程实例
     * @param task 当然任务
     * @param otherParams 其他参数Map
     */
    public Object eval(String script, Map form, ProcessInstance processInst, ToDoTask task, Map otherParams = null) {
        // 替换公式
        script = calScript(script)
        // 脚本input参数
        Binding binding = new Binding()
        binding.setVariable("log", log)
        binding.setVariable("form", form)
        binding.setVariable("pi", processInst)    // 流程实例
        binding.setVariable("t", task)            // 任务实例
        binding.setVariable("task", task)         // 任务实例

        binding.setVariable("userService", userService)
//        binding.setVariable("session", ServletUtils.getSession())                    // 当前http session（如果有的话）

        // 其他参数
        if (otherParams) for (p in otherParams) binding.setVariable(p.key, p.value)
//        // 修改脚本头，导入包
//        StringBuilder sb = new StringBuilder()
//        sb << "import app.*;\n"
//        sb << script
//        script = sb.toString()
        // 运行
        try {
            return evalScript(script, binding, null)
        } catch (Exception e) {
            log.error("流程脚本执行时异常：" + "\n" + script, e)
            throw new Exception("流程脚本执行时异常", e)
        }
    }

    /**
     * 执行脚本：使用GSE，避免每次都编译<p>
     *
     * @param script 脚本内容
     * @param binding 参数绑定，可为空
     * @param params 其他参数，可为空
     * @return
     */
    private Object evalScript(String script, Binding binding, Map params) {
        //http://blog.oio.de/2013/07/02/groovy-scriptengine-memory-leak-fixed/
        //script = script.intern()
        // 处理参数
        if (!binding) binding = new Binding()
        if (params) for (p in params) binding.setVariable(p.key, p.value)
        if (!binding.hasVariable("log")) binding.setVariable("log", log)
        javax.servlet.ServletContext ctx = grailsApplication.getMainContext().getServletContext()
        // 初始化GSE：同步，避免冲突
        synchronized (ProcessScriptService.class) {
            if (ctx.gse == null) {
                // 初始化GSE，监控目录 /tmp/GSE-xxxx/
                ctx.gseRoot = System.getProperty("java.io.tmpdir") + System.getProperty("file.separator")
                // TODO bug
//                ctx.gseRoot += "GSE-" + ServletUtils.contextPath.substring(1)
                File dir = new File(ctx.gseRoot)
                // 每隔一周，自动清理一次源代码文件夹
                if (dir.exists() && dir.lastModified() + 604800000 < System.currentTimeMillis()) {
                    try {
                        log.info("Clean GSE root directory: " + ctx.gseRoot)
                        org.apache.commons.io.FileUtils.deleteDirectory(dir)
                        dir = new File(ctx.gseRoot)
                    } catch (IOException e) {
                        log.error("Clean GSE root directory failed!", e)
                    }
                }
                if (!dir.exists()) dir.mkdirs()
                // 初始化GSE
                ctx.gse = new GroovyScriptEngine(ctx.gseRoot, Thread.currentThread().contextClassLoader)
                ctx.gse.config.setSourceEncoding("UTF-8")
            }
        }
        // 将脚本写入文件：如果怕hashCode还有重复的，以后可以考虑把脚本长度加上
        String fileName = "script" + script.hashCode() + ".groovy"
        String fullname = ctx.gseRoot + System.getProperty("file.separator") + fileName
        File f = new File(fullname)
        if (!f.exists()) {
            // 导入常用类
            for (clazz in getAutoImportClasses()) {
                f << "import " << clazz << ";"
            }
            // Windows下：将汉字转码成unicode的形式，如“\u4F60”，以规避返回字符串的乱码问题、对象无法解析等问题
            // http://groovy.329449.n5.nabble.com/GroovyScriptEngine-return-messy-code-at-Windows-td5710184.html
            if ((System.getProperty("os.name").indexOf("Windows") == 0)) {
                // TODO bug
//                f << StringUtils.toUnicode(script)
                f << script
            } else f << script
        }
        // 执行并返回
        log.trace("Execute groovy script: " + fileName)
        return ctx.gse.run(fileName, binding)
    }

    /**
     * 递归获取脚本代码：替换公式为脚本
     *
     * <p>FIXED 替换公式时，避免把 $ABC 当作 $AB 替换掉，导致错误 -- 转成StringBuilder后，用replace方法！</p>
     *
     * @param script 需要处理的脚本
     * @param formulaScripts 本次计算涉及到的公式对应实际脚本的缓存，仅用于内部递归，因此调用时无需本参数
     */
    private String calScript(String script, Map formulaScripts = null) {
        if (script?.indexOf("\$") > -1) {
            Matcher m = (script =~ p)
            StringBuilder sb = null
            int start = 0, offset = 0, len
            // 判断是否包含公式，并循环
            while (m.find(start)) {
                // 初始化临时变量
                if (sb == null) {
                    len = script.length()
                    sb = new StringBuilder(script)
                    if (formulaScripts == null) formulaScripts = [:]
                }
                // 把原script中的code(公式代码) 替换成script（脚本内容）并递归传递替换后的script
                String code = m.group(1).substring(1)
                // 替换公式
                if (formulaScripts[code]) {
                    //println "code=" + code + "=" + formulaScripts[code]
                    sb.replace(m.start() + offset, m.end() + offset - (m.end() == len ? 0 : 1), formulaScripts[code])
                    offset += formulaScripts[code].length() - code.length() - 1
                    //println "m.start()=" + m.start() + ", m.end()=" + m.end() + ", offset=" + offset
                }
                start = m.end()
            }
            // 如果替换过公式，则返回新的脚本：用offset来做判断条件，避免没找到对应公式后，多做一次toString！
            if (offset > 0) return sb.toString()
        }
        return script
    }

    /**
     * 根据配置 bropen.toolkit.autoimport.classes.xxxx 获得将要在GSP、Script中自动导入的类列表
     */
    private List<String> getAutoImportClasses() {
        if (_autoImportClasses == null) {
            synchronized (ProcessScriptService.class) {
                ConfigObject config = Holders.getGrailsApplication().config
                _autoImportClasses = []
                for (c in config.bropen.toolkit.autoimport.classes) {
                    if (c.value instanceof List)
                        _autoImportClasses.addAll(c.value)
                }
            }
        }
        return _autoImportClasses
    }
    private static List<String> _autoImportClasses = null
}